package com.bobo.mm.socket.decoder;

import android.util.Log;

import com.bobo.mm.exception.TooLongFrameException;

import java.util.List;

import io.netty.buffer.ByteBuf;
import io.netty.util.ByteProcessor;

/**
 * 基于\n或者\r\n分隔符对数据进行解码，参考netty源码实现
 * Created by cocolove2 on 2020/8/21.
 */
public class LineBasedFrameDecoder implements IDecoder {
    private static final String TAG = "LineBasedFrameDecoder";
    /**
     * Maximum length of a frame we're willing to decode.
     */
    private final int maxLength;
    /**
     * Whether or not to throw an exception as soon as we exceed maxLength.
     */
    private final boolean failFast;

    private final boolean stripDelimiter;
    /**
     * True if we're discarding input because we're already over maxLength.
     */
    private boolean discarding;
    private int discardedBytes;

    /**
     * Last scan position.
     */
    private int offset;

    /**
     * Creates a new decoder.
     *
     * @param maxLength the maximum length of the decoded frame.
     *                  A {@link TooLongFrameException} is thrown if
     *                  the length of the frame exceeds this value.
     */
    public LineBasedFrameDecoder(final int maxLength) {
        this(maxLength, true, false);
    }

    /**
     * Creates a new decoder.
     *
     * @param maxLength      the maximum length of the decoded frame.
     *                       A {@link TooLongFrameException} is thrown if
     *                       the length of the frame exceeds this value.
     * @param stripDelimiter whether the decoded frame should strip out the
     *                       delimiter or not
     * @param failFast       If <tt>true</tt>, a {@link TooLongFrameException} is
     *                       thrown as soon as the decoder notices the length of the
     *                       frame will exceed <tt>maxFrameLength</tt> regardless of
     *                       whether the entire frame has been read.
     *                       If <tt>false</tt>, a {@link TooLongFrameException} is
     *                       thrown after the entire frame that exceeds
     *                       <tt>maxFrameLength</tt> has been read.
     */
    public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
        this.maxLength = maxLength;
        this.failFast = failFast;
        this.stripDelimiter = stripDelimiter;
    }


    @Override
    public void decode(String clientId, ByteBuf in, List<byte[]> frames) {

        ByteBuf decoded = decode(in);
        if (decoded != null) {
            byte[] frame = new byte[decoded.readableBytes()];
            decoded.readBytes(frame);
            decoded.release();
            frames.add(frame);
        }
        //缓存区数据读完，重置
        if (in.readerIndex() == in.writerIndex()) {
            in.clear();
        } else {
            Log.i(TAG, "缓存区还有未读内容,缓存容量:" + in.capacity() + ",可写容量:" + in.writableBytes());
        }
    }


    private ByteBuf decode(ByteBuf buffer) {
        final int eol = findEndOfLine(buffer);
        if (!discarding) {
            if (eol >= 0) {
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;

                if (length > maxLength) {
                    buffer.readerIndex(eol + delimLength);
                    fail(length);
                    return null;
                }

                if (stripDelimiter) {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            } else {
                final int length = buffer.readableBytes();
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    offset = 0;
                    if (failFast) {
                        fail("over " + discardedBytes);
                    }
                }
                return null;
            }
        } else {
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    fail(length);
                }
            } else {
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
                // We skip everything in the buffer, we need to set the offset to 0 again.
                offset = 0;
            }
            return null;
        }
    }

    private void fail(int length) {
        fail(String.valueOf(length));
    }

    private void fail(String length) {
        throw new TooLongFrameException(
                "frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')');
    }

    /**
     * Returns the index in the buffer of the end of line found.
     * Returns -1 if no end of line was found in the buffer.
     */
    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }
}
